/*
 * This file is part of NativeAccessHooks.
 *
 * Copyleft 2014 Mark Jeronimus. All Rights Reversed.
 * https://code.google.com/p/nativeaccesshooks/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NativeAccessHooks. If not, see <http://www.gnu.org/licenses/>.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.digitalmodular.nah;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.event.EventListenerList;

import com.digitalmodular.nah.keyboard.NativeKeyEvent;
import com.digitalmodular.nah.keyboard.NativeKeyListener;
import com.digitalmodular.nah.mouse.NativeMouseButtonEvent;
import com.digitalmodular.nah.mouse.NativeMouseButtonListener;
import com.digitalmodular.nah.mouse.NativeMouseMotionEvent;
import com.digitalmodular.nah.mouse.NativeMouseMotionListener;
import com.digitalmodular.nah.mouse.NativeMouseWheelEvent;
import com.digitalmodular.nah.mouse.NativeMouseWheelListener;

/**
 * HookManager manages the hooks to register and unregister. It is important to realize that hooks must be unregistered properly
 * after registering them, otherwise windows will remember them even after closing the application. They will not be unregistered in
 * the same way files are closed on exit. Terminating an application from an IDE or Task Manager is a big NO. If the hook queue of
 * windows gets full, it will fail to register new hooks. This means that all applications that use the same hooks (some games for
 * example) will have dead inputs.
 *
 * To register hooks, you first have to tell this manager which hooks you want to register, using {@link #addNativeHook(Class)}.
 * Then you can register them by calling {@link #registerNativeHooks()}. After registering, Hooks cannot be added or removed
 * anymore, so the logical place to do this is at the start of the application.
 *
 * At any time, you can add or remove {@link NativeInputEvent} listeners to this class (<i>e.g.</i>
 * {@link #addNativeKeyListener(NativeKeyListener)}), in the same way you would add or remove listeners to a {@link JFrame}.
 *
 * When the application should close, call {@link #unregisterNativeHooksAndExit(int)} instead of {@link System#exit(int)}. For
 * safety reasons, this is the only way to unregister hooks. The parameter is the familiar exit status code.`
 *
 * @author Mark Jeronimus
 * @since 1.0
 */
// date 2014-08-23
public class HookManager {
	// /** The HookManager singleton. */
	// private static final HookManager INSTANCE = new HookManager();

	/** The classes of the hooks that the user wants to register. This instance is also used for synchronizing on. */
	static final List<NativeHook>					hookInstances	= new ArrayList<>();

	/** The currently registered hooks. */
	static final List<Class<? extends NativeHook>>	hooks			= new ArrayList<>();

	/** The list of event listeners. */
	static final EventListenerList					eventListeners	= new EventListenerList();

	/**
	 * Static class.
	 */
	private HookManager() {
		throw new AssertionError();
	}

	/**
	 * Adds the class of a {@link NativeHook} to the set of hooks that the user wants to register.
	 *
	 * @param hookClass the class of the hook to include in the set.
	 * @throws IllegalStateException if the hooks are already registered by {@link #registerNativeHooks()}.
	 */
	public static void addNativeHook(Class<? extends NativeHook> hookClass) {
		synchronized (hookInstances) {
			// Already registered?
			if (!hookInstances.isEmpty()) {
				throw new IllegalStateException("Can't add a hook when the hooks are already registered.");
			}

			if (!hooks.contains(hookClass)) {
				hooks.add(hookClass);
			}
		}
	}

	/**
	 * Registers the native hooks if they are not currently registered. If there are no hook classes added, then this call has no
	 * effect. If they are registered, the function also has no effect.
	 */
	public static void registerNativeHooks() {
		synchronized (hookInstances) {
			// Already registered?
			if (!hookInstances.isEmpty()) {
				return;
			}

			for (Class<? extends NativeHook> hookClass : hooks) {
				try {
					Constructor<? extends NativeHook> constructor = hookClass.getConstructor(new Class[]{});
					NativeHook hook = constructor.newInstance(new Object[]{});

					if (hook.isRegistered()) {
						hookInstances.add(hook);
					}
				}
				catch (ReflectiveOperationException | SecurityException | IllegalArgumentException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * Disables all native hooks if they're currently registered.
	 *
	 * @param status exit status.
	 */
	public static void unregisterNativeHooksAndExit(int status) {
		synchronized (hookInstances) {
			for (NativeHook hook : hookInstances) {
				hook.unregisterHook();
			}
			hookInstances.clear();
		}
		System.exit(status);
	}

	/**
	 * Adds the specified listener to receive key events from the manager. If listener is null, no exception is thrown and no action
	 * is performed.
	 *
	 * @param listener a native key listener object
	 */
	public static void addNativeKeyListener(NativeKeyListener listener) {
		if (listener != null) {
			eventListeners.add(NativeKeyListener.class, listener);
		}
	}

	/**
	 * Removes the specified listener so that it no longer receives window events from this window. If listener is null, no
	 * exception is thrown and no action is performed.
	 *
	 * @param listener a native key listener object
	 */
	public static void removeNativeKeyListener(NativeKeyListener listener) {
		if (listener != null) {
			eventListeners.remove(NativeKeyListener.class, listener);
		}
	}

	/**
	 * Adds the specified listener to receive key events from the manager. If listener is null, no exception is thrown and no action
	 * is performed.
	 *
	 * @param listener a native mouse listener object
	 */
	public static void addNativeMouseButtonListener(NativeMouseButtonListener listener) {
		if (listener != null) {
			eventListeners.add(NativeMouseButtonListener.class, listener);
		}
	}

	/**
	 * Removes the specified listener so that it no longer receives window events from this window. If listener is null, no
	 * exception is thrown and no action is performed.
	 *
	 * @param listener a native mouse listener object
	 */
	public static void removeNativeMouseListener(NativeMouseButtonListener listener) {
		if (listener != null) {
			eventListeners.remove(NativeMouseButtonListener.class, listener);
		}
	}

	/**
	 * Adds the specified listener to receive key events from the manager. If listener is null, no exception is thrown and no action
	 * is performed.
	 *
	 * @param listener a native mouse motion listener object
	 */
	public static void addNativeMouseMotionListener(NativeMouseMotionListener listener) {
		if (listener != null) {
			eventListeners.add(NativeMouseMotionListener.class, listener);
		}
	}

	/**
	 * Removes the specified listener so that it no longer receives window events from this window. If listener is null, no
	 * exception is thrown and no action is performed.
	 *
	 * @param listener a native mouse motion listener object
	 */
	public static void removeNativeMouseMotionListener(NativeMouseMotionListener listener) {
		if (listener != null) {
			eventListeners.remove(NativeMouseMotionListener.class, listener);
		}
	}

	/**
	 * Adds the specified listener to receive key events from the manager. If listener is null, no exception is thrown and no action
	 * is performed.
	 *
	 * @param listener a native mouse wheel listener object
	 */
	public static void addNativeMouseWheelListener(NativeMouseWheelListener listener) {
		if (listener != null) {
			eventListeners.add(NativeMouseWheelListener.class, listener);
		}
	}

	/**
	 * Removes the specified listener so that it no longer receives window events from this window. If listener is null, no
	 * exception is thrown and no action is performed.
	 *
	 * @param listener a native mouse wheel listener object
	 */
	public static void removeNativeMouseWheelListener(NativeMouseWheelListener listener) {
		if (listener != null) {
			eventListeners.remove(NativeMouseWheelListener.class, listener);
		}
	}

	/**
	 * Dispatches an event to the appropriate event listener. This method is generally called by the {@link NativeHook}s but may be
	 * used to emulate native events from Java.
	 *
	 * @param e the {@link NativeInputEvent} to be sent to the appropriate event listeners.
	 */
	public final static void dispatchEvent(final NativeInputEvent e) {
		RuntimeException thrown = null;
		if (e instanceof NativeMouseMotionEvent) {
			final NativeMouseMotionEvent e2 = (NativeMouseMotionEvent)e;
			for (NativeMouseMotionListener listener : eventListeners.getListeners(NativeMouseMotionListener.class)) {
				try {
					listener.nativeMouseMoved(e2);
				}
				catch (RuntimeException e1) {
					thrown = e1;
				}
			}
		} else if (e instanceof NativeMouseButtonEvent) {
			final NativeMouseButtonEvent e2 = (NativeMouseButtonEvent)e;
			switch (e2.getEventType()) {
				case ButtonPressed:
					for (NativeMouseButtonListener listener : eventListeners.getListeners(NativeMouseButtonListener.class)) {
						try {
							listener.nativeMousePressed(e2);
						}
						catch (RuntimeException e1) {
							thrown = e1;
						}
					}
					break;
				case ButtonReleased:
					for (NativeMouseButtonListener listener : eventListeners.getListeners(NativeMouseButtonListener.class)) {
						try {
							listener.nativeMouseReleased(e2);
						}
						catch (RuntimeException e1) {
							thrown = e1;
						}
					}
					break;
			}
		} else if (e instanceof NativeMouseWheelEvent) {
			final NativeMouseWheelEvent e2 = (NativeMouseWheelEvent)e;
			for (NativeMouseWheelListener listener : eventListeners.getListeners(NativeMouseWheelListener.class)) {
				try {
					listener.nativeMouseWheelMoved(e2);
				}
				catch (RuntimeException e1) {
					thrown = e1;
				}
			}
		} else if (e instanceof NativeKeyEvent) {
			final NativeKeyEvent e2 = (NativeKeyEvent)e;
			switch (e2.getEventType()) {
				case Pressed:
					for (NativeKeyListener listener : eventListeners.getListeners(NativeKeyListener.class)) {
						try {
							listener.nativeKeyPressed(e2);
						}
						catch (RuntimeException e1) {
							thrown = e1;
						}
					}
					break;
				case Released:
					for (NativeKeyListener listener : eventListeners.getListeners(NativeKeyListener.class)) {
						try {
							listener.nativeKeyReleased(e2);
						}
						catch (RuntimeException e1) {
							thrown = e1;
						}
					}
					break;
			}
		}

		// Handle uncaught exception.
		if (thrown != null) {
			thrown.printStackTrace();
		}
	}
}
